package de.ubschmidt.capi;

import de.ubschmidt.capi.util.StringUtil;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Map;
import org.capi.capi20.Capi;
import org.capi.capi20.CapiException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implementation of BinTec's <b>Remote CAPI over TCP/IP</b>.
 */
public class RCapi
        implements Capi {

    private static final Logger logger = LoggerFactory.getLogger(RCapi.class);

    private static final byte CAPI_VERSION_2 = (byte) 2;

    private static final short MANUFACTURER_REQ = (short) 0xfffa;
    private static final short MANUFACTURER_CONF = (short) 0xfffb;
    private static final short PROFILE_REQ = (short) 0xffe0;
    private static final short PROFILE_CONF = (short) 0xffe1;
    private static final short REGISTER_REQ = (short) 0xfff2;
    private static final short REGISTER_CONF = (short) 0xfff3;
    private static final short SERIAL_NUMBER_REQ = (short) 0xfffe;
    private static final short SERIAL_NUMBER_CONF = (short) 0xffff;
    private static final short VERSION_REQ = (short) 0xfffc;
    private static final short VERSION_CONF = (short) 0xfffd;

    /** XXX */
    private static final short NO_MESSAGE = (short) 0x11fe;
    private static final short RCAPI_FAILURE = (short) 0x11ff;

    private static final int HEADER_LENGTH = 8;

    private final String _brickHost;
    private final int _brickPort;

    /**
     * Creates a new instance of {@code RCapi}.
     *
     * @param brickHost
     * @param brickPort
     */
    public RCapi(final String brickHost, final int brickPort) {
        logger.debug("RCapi(brickHost={}, brickPort={})", brickHost, brickPort);

        _brickHost = brickHost;
        _brickPort = brickPort;
    }

    /** {@inheritDoc} */
    public boolean installed()
            throws CapiException {
        logger.debug("installed()");

        return true;
    }

    /** {@inheritDoc} */
    public short register(final short messageBufferSize,
            final short maxLogicalConnections,
            final short maxBDataBlocks,
            final short maxBDataLen)
            throws CapiException {
        logger.debug("register()");

        short appId = openConnection();
        boolean succeeded = false;

        try {
            ByteBuffer byteBuffer = createMessage(21, appId, REGISTER_REQ);
            byteBuffer.putInt(0); // buffer pointer (currently not used)
            byteBuffer.putShort(messageBufferSize); // (currently not used)
            byteBuffer.putShort(maxLogicalConnections);
            byteBuffer.putShort(maxBDataBlocks);
            byteBuffer.putShort(maxBDataLen);
            byteBuffer.put(CAPI_VERSION_2);

            putMessage(appId, byteBuffer);

            waitForSignal(appId);
            byte [] buf = getMessage(appId);

            byteBuffer = verifyAndWrapMessage(buf, appId, REGISTER_CONF);

            short errorInfo = byteBuffer.getShort(HEADER_LENGTH);
            if (errorInfo != 0) {
                throw new CapiException(errorInfo);
            }

            succeeded = true;
        } finally {
            if (!succeeded) {
                closeConnection(appId);
            }
        }

        return appId;
    }

    /** {@inheritDoc} */
    public void release(final short appId)
            throws CapiException {
        logger.debug("release()");

        closeConnection(appId);
    }

    private void putMessage(final short appId, final ByteBuffer capiMessage)
            throws CapiException {
        byte [] buf = new byte [capiMessage.limit()];
        capiMessage.rewind();
        capiMessage.get(buf);

        putMessage(appId, buf);
    }

    /** {@inheritDoc} */
    public void putMessage(final short appId, final byte [] msg)
            throws CapiException {
        int length = msg.length + 2;

        ByteBuffer byteBuffer = ByteBuffer.allocate(length);
        byteBuffer.putShort((short) length);
        byteBuffer.put(msg);
        byteBuffer.rewind();

        try {
            SocketChannel socketChannel = _sockets.get(appId);
            int rc = socketChannel.write(byteBuffer);
        } catch (IOException e) {
            closeConnection(appId);
            throw new CapiException(RCAPI_FAILURE, "IOException", e);
        }
    }

    /** {@inheritDoc} */
    public byte [] getMessage(final short appId)
            throws CapiException {
        logger.debug("getMessage(): appId = {}", appId);

        try {
            SocketChannel socketChannel = _sockets.get(appId);

            Selector selector = Selector.open();
            socketChannel.register(selector, SelectionKey.OP_READ);
            if (selector.selectNow() == 0) {
                logger.debug("getMessage(): no message pending, "
                        + "consider using waitForSignal() first.");
                throw new CapiException(NO_MESSAGE, "no message pending");
            }

            int bytesRead = 0;
            ByteBuffer byteBuffer = ByteBuffer.allocate(2);

            while (bytesRead != 2) {
                int len = socketChannel.read(byteBuffer);
                if (len == -1) {
                    closeConnection(appId);
                    throw new CapiException(RCAPI_FAILURE,
                            "premature end of input stream");
                }
                bytesRead += len;
            }

            int length = byteBuffer.getShort(0) - 2;

            byteBuffer = ByteBuffer.allocate(length);

            bytesRead = 0;
            while (bytesRead != length) {
                int len = socketChannel.read(byteBuffer);
                if (len == -1) {
                    closeConnection(appId);
                    throw new CapiException(RCAPI_FAILURE,
                            "premature end of input stream");
                }
                bytesRead += len;
            }

            return byteBuffer.array();
        } catch (IOException e) {
            closeConnection(appId);
            throw new CapiException(RCAPI_FAILURE,
                    "IOException", e);
        }
    }

    /** {@inheritDoc} */
    public void waitForSignal(final short appId)
            throws CapiException {
        logger.debug("waitForSignal()");

        SocketChannel socketChannel = _sockets.get(appId);

        try {
            Selector selector = Selector.open();

            socketChannel.register(selector, SelectionKey.OP_READ);
            selector.select();
        } catch (IOException e) {
            closeConnection(appId);
            throw new CapiException(RCAPI_FAILURE,
                    "waitForSignal() failed", e);
        }
    }

    /** {@inheritDoc} */
    public String getManufacturer(final int controller)
            throws CapiException {
        short appId = openConnection();

        ByteBuffer byteBuffer = createMessage(HEADER_LENGTH, appId,
                MANUFACTURER_REQ);

        try {
            putMessage(appId, byteBuffer);

            waitForSignal(appId);
            byte [] buf = getMessage(appId);

            byteBuffer = verifyAndWrapMessage(buf, appId, MANUFACTURER_CONF);

            return StringUtil.szString(
                    CapiStruct.readStructContent(
                    (ByteBuffer) byteBuffer.position(HEADER_LENGTH)));
        } finally {
            closeConnection(appId);
        }
    }

    /** {@inheritDoc} */
    public short [] getVersion(final int controller)
            throws CapiException {
        final int OFFSET_CAPI_VERSION = HEADER_LENGTH;

        short appId = openConnection();

        ByteBuffer byteBuffer = createMessage(HEADER_LENGTH, appId,
                VERSION_REQ);

        try {
            putMessage(appId, byteBuffer);

            waitForSignal(appId);
            byte [] buf = getMessage(appId);

            byteBuffer = verifyAndWrapMessage(buf, appId, VERSION_CONF);

            short [] result = new short [4];

            result[0] = CAPI_VERSION_2;
            result[1] = 0;

            short manufacturerVersion = byteBuffer.getShort(HEADER_LENGTH);

            result[2] = (short) (manufacturerVersion / 100);
            result[3] = (short) (manufacturerVersion % 100);

            return result;
        } finally {
            closeConnection(appId);
        }
    }

    /** {@inheritDoc} */
    public String getSerialNumber(final int controller)
            throws CapiException {
        short appId = openConnection();

        ByteBuffer byteBuffer = createMessage(HEADER_LENGTH, appId,
                SERIAL_NUMBER_REQ);

        try {
            putMessage(appId, byteBuffer);

            waitForSignal(appId);
            byte [] buf = getMessage(appId);

            byteBuffer = verifyAndWrapMessage(buf, appId, SERIAL_NUMBER_CONF);

            return StringUtil.szString(
                    CapiStruct.readStructContent(
                    (ByteBuffer) byteBuffer.position(HEADER_LENGTH)));
        } finally {
            closeConnection(appId);
        }
    }

    /** {@inheritDoc} */
    public byte [] getProfile(final int controller)
            throws CapiException {
        short appId = openConnection();

        ByteBuffer byteBuffer = createMessage(10, appId, PROFILE_REQ);
        byteBuffer.putShort((short) controller);

        try {
            putMessage(appId, byteBuffer);

            waitForSignal(appId);
            byte [] buf = getMessage(appId);

            byteBuffer = verifyAndWrapMessage(buf, appId, PROFILE_CONF);

            short info = byteBuffer.getShort(HEADER_LENGTH);

            if (info != 0) {
                throw new CapiException(info);
            }

            byteBuffer.position(HEADER_LENGTH + 2);
            int length = byteBuffer.remaining();
            buf = new byte [length];
            byteBuffer.get(buf, 0, length);

            return buf;
        } finally {
            closeConnection(appId);
        }
    }

    /**
     * (Possibly) the next application ID.
     *
     * @see #getNextAppId()
     */
    private short _nextAppId = 1;

    /**
     * Create and return a new valid application ID.
     *
     * @return A new application ID.
     */
    private synchronized short getNextAppId() {
        short appId;

        do {
            appId = _nextAppId++;
        } while (appId == 0 || _sockets.containsKey(appId));

        return appId;
    }

    private short openConnection()
            throws CapiException {
        logger.debug("openConnection()");

        try {
            SocketAddress socketAddress =
                    new InetSocketAddress(_brickHost, _brickPort);
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            socketChannel.connect(socketAddress);

            Selector selector = Selector.open();
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
            selector.select();
            socketChannel.finishConnect();

            synchronized (_sockets) {
                short appId = getNextAppId();
                _sockets.put(appId, socketChannel);

                return appId;
            }
        } catch (UnknownHostException e) {
            throw new CapiException(RCAPI_FAILURE,
                    "Unknown Host", e);
        } catch (IOException e) {
            throw new CapiException(RCAPI_FAILURE,
                    "IOException", e);
        }
    }

    private void closeConnection(final short appId) {
        logger.debug("closeConnection()");

        SocketChannel socketChannel;

        synchronized (_sockets) {
            socketChannel = _sockets.remove(appId);
        }

        if (socketChannel == null) {
            return;
        }

        try {
            socketChannel.close();
        } catch (IOException e) {
            logger.error("closeConnection(): caught IOException", e);
        }
    }

    /** Map of application IDs to Sockets. */
    private Map<Short,SocketChannel> _sockets =
            new HashMap<Short,SocketChannel>();

    private ByteBuffer createMessage(final int length, final short appId,
            final short command) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(length);
        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);

        byteBuffer.putShort((short) length);
        byteBuffer.putShort(appId);
        byteBuffer.putShort(command);
        byteBuffer.putShort((short) 0); // message number

        return byteBuffer;
    }

    private ByteBuffer verifyAndWrapMessage(final byte [] buf,
            final short appId, final short command) {
        ByteBuffer byteBuffer = ByteBuffer.wrap(buf);
        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);

        //assert byteBuffer.getShort(2) == appId;
        assert byteBuffer.getShort(4) == command;
        assert byteBuffer.getShort(6) == 0; // message number

        return byteBuffer;
    }

}
